package config

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"math/big"
	"os"
	"path"
	"slices"
	"sync"
	"time"

	"github.com/ethereum-optimism/optimism/cannon/mipsevm/versions"
	"github.com/ethereum-optimism/optimism/op-core/forks"
	"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
	"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts"
	"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect"
	"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/pipeline"
	"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
	"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"golang.org/x/exp/maps"

	"github.com/ethereum-optimism/optimism/op-e2e/config/secrets"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/log"

	"github.com/ethereum-optimism/optimism/op-chain-ops/addresses"
	"github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
	"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
	op_service "github.com/ethereum-optimism/optimism/op-service"
	oplog "github.com/ethereum-optimism/optimism/op-service/log"

	_ "embed"
)

// legacy geth log levels - the geth command line --verbosity flag wasn't
// migrated to use slog's numerical levels.
const (
	LegacyLevelCrit = iota
	LegacyLevelError
	LegacyLevelWarn
	LegacyLevelInfo
	LegacyLevelDebug
	LegacyLevelTrace
)

type AllocType string

const (
	AllocTypeAltDA        AllocType = "alt-da"
	AllocTypeMTCannon     AllocType = "mt-cannon"
	AllocTypeMTCannonNext AllocType = "mt-cannon-next"
	AllocTypeFastGame     AllocType = "fast-game"

	DefaultAllocType = AllocTypeMTCannon
)

func (a AllocType) Check() error {
	if !slices.Contains(allocTypes, a) {
		return fmt.Errorf("unknown alloc type: %q", a)
	}
	return nil
}

var allocTypes = []AllocType{AllocTypeAltDA, AllocTypeMTCannon, AllocTypeMTCannonNext, AllocTypeFastGame}

var (
	// All of the following variables are set in the init function
	// and read from JSON files on disk that are generated by the
	// foundry deploy script. These are globally exported to be used
	// in end to end tests.

	// L1Allocs represents the L1 genesis block state.
	l1AllocsByType = make(map[AllocType]*foundry.ForgeAllocs)
	// L1Deployments maps contract names to accounts in the L1
	// genesis block state.
	l1DeploymentsByType = make(map[AllocType]*genesis.L1Deployments)
	// l2Allocs represents the L2 allocs, by hardfork/mode (e.g. delta, ecotone, interop, other)
	l2AllocsByType = make(map[AllocType]genesis.L2AllocsModeMap)
	// DeployConfig represents the deploy config used by the system.
	deployConfigsByType = make(map[AllocType]*genesis.DeployConfig)
	// EthNodeVerbosity is the (legacy geth) level of verbosity to output
	EthNodeVerbosity int = 3

	// mtx is a lock to protect the above variables
	mtx sync.RWMutex
)

func L1Allocs(allocType AllocType) *foundry.ForgeAllocs {
	mtx.RLock()
	defer mtx.RUnlock()
	allocs, ok := l1AllocsByType[allocType]
	if !ok {
		panic(fmt.Errorf("unknown L1 alloc type: %q", allocType))
	}
	return allocs.Copy()
}

func L1Deployments(allocType AllocType) *genesis.L1Deployments {
	mtx.RLock()
	defer mtx.RUnlock()
	deployments, ok := l1DeploymentsByType[allocType]
	if !ok {
		panic(fmt.Errorf("unknown L1 deployments type: %q", allocType))
	}
	return deployments.Copy()
}

func L2Allocs(allocType AllocType, mode genesis.L2AllocsMode) *foundry.ForgeAllocs {
	mtx.RLock()
	defer mtx.RUnlock()
	allocsByType, ok := l2AllocsByType[allocType]
	if !ok {
		panic(fmt.Errorf("unknown L2 alloc type: %q", allocType))
	}

	allocs, ok := allocsByType[mode]
	if !ok {
		panic(fmt.Errorf("unknown L2 allocs mode: %q", mode))
	}
	return allocs.Copy()
}

func DeployConfig(allocType AllocType) *genesis.DeployConfig {
	mtx.RLock()
	defer mtx.RUnlock()
	dc, ok := deployConfigsByType[allocType]
	if !ok {
		panic(fmt.Errorf("unknown deploy config type: %q", allocType))
	}
	return dc.Copy()
}

func init() {
	// Used by the rust team, to skip legacy op-e2e init. Not used by devstack acceptance tests.
	if os.Getenv("DISABLE_OP_E2E_LEGACY") == "true" {
		return
	}
	cwd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	root, err := op_service.FindMonorepoRoot(cwd)
	if err != nil {
		panic(err)
	}

	// Setup global logger
	lvl := log.FromLegacyLevel(EthNodeVerbosity)
	var handler slog.Handler
	var errHandler slog.Handler
	if lvl > log.LevelCrit {
		handler = log.DiscardHandler()
		errHandler = log.DiscardHandler()
	} else {
		if lvl < log.LevelTrace { // clip to trace level
			lvl = log.LevelTrace
		}
		// We cannot attach a testlog logger,
		// because the global logger is shared between different independent parallel tests.
		// Tests that write to a testlogger of another finished test fail.
		handler = oplog.NewLogHandler(os.Stdout, oplog.CLIConfig{
			Level:  lvl,
			Color:  false, // some CI logs do not handle colors well
			Format: oplog.FormatTerminal,
		})

		errHandler = oplog.NewLogHandler(os.Stderr, oplog.CLIConfig{
			Level:  log.LevelError,
			Color:  false,
			Format: oplog.FormatTerminal,
		})
	}

	// Start at warning level since alloc generation is heavy on the logs,
	// which reduces CI performance.
	oplog.SetGlobalLogHandler(errHandler)

	for _, allocType := range allocTypes {
		initAllocType(root, allocType)
	}

	// Use regular level going forward.
	oplog.SetGlobalLogHandler(handler)
}

func initAllocType(root string, allocType AllocType) {
	artifactsPath := path.Join(root, "packages", "contracts-bedrock", "forge-artifacts")
	if err := ensureDir(artifactsPath); err != nil {
		panic(fmt.Errorf("invalid artifacts path: %w", err))
	}

	loc, err := artifacts.NewFileLocator(artifactsPath)
	if err != nil {
		panic(fmt.Errorf("failed to create artifacts locator: %w", err))
	}

	lgr := log.New()

	allocModes := []genesis.L2AllocsMode{
		genesis.L2AllocsInterop,
		genesis.L2AllocsJovian,
		genesis.L2AllocsIsthmus,
		genesis.L2AllocsHolocene,
		genesis.L2AllocsGranite,
		genesis.L2AllocsFjord,
		genesis.L2AllocsEcotone,
		genesis.L2AllocsDelta,
	}

	l2Alloc := make(map[genesis.L2AllocsMode]*foundry.ForgeAllocs)
	var wg sync.WaitGroup

	pk := secrets.DefaultSecrets.Deployer
	deployerAddr := crypto.PubkeyToAddress(pk.PublicKey)
	lgr.Info("deployer address", "address", deployerAddr.Hex())

	for _, mode := range allocModes {
		wg.Add(1)
		go func(mode genesis.L2AllocsMode) {
			defer wg.Done()

			intent := defaultIntent(root, loc, deployerAddr, allocType)
			if allocType == AllocTypeAltDA {
				intent.Chains[0].DangerousAltDAConfig = genesis.AltDADeployConfig{
					UseAltDA:                   true,
					DACommitmentType:           "KeccakCommitment",
					DAChallengeWindow:          16,
					DAResolveWindow:            16,
					DABondSize:                 1000000,
					DAResolverRefundPercentage: 0,
				}
			}
			if allocType == AllocTypeFastGame {
				intent.GlobalDeployOverrides["preimageOracleChallengePeriod"] = 1
				for _, chain := range intent.Chains {
					chain.AdditionalDisputeGames = append(chain.AdditionalDisputeGames,
						state.AdditionalDisputeGame{
							ChainProofParams: state.ChainProofParams{
								// Fast game
								DisputeGameType: 254,
								// Prestate doesn't matter as there's no time to play the game anyway.
								DisputeAbsolutePrestate: common.HexToHash("0x03c7ae758795765c6664a5d39bf63841c71ff191e9189522bad8ebff5d4eca98"),
								DisputeMaxGameDepth:     14 + 3 + 1,
								DisputeSplitDepth:       14,
								DisputeClockExtension:   0,
								DisputeMaxClockDuration: 1,
							},
							VMType:        state.VMTypeAlphabet,
							MakeRespected: true,
						})
				}
			}

			baseUpgradeSchedule := map[string]any{
				"l2GenesisRegolithTimeOffset": nil,
				"l2GenesisCanyonTimeOffset":   nil,
				"l2GenesisDeltaTimeOffset":    nil,
				"l2GenesisEcotoneTimeOffset":  nil,
				"l2GenesisFjordTimeOffset":    nil,
				"l2GenesisGraniteTimeOffset":  nil,
				"l2GenesisHoloceneTimeOffset": nil,
				"l2GenesisIsthmusTimeOffset":  nil,
				"l2GenesisJovianTimeOffset":   nil,
			}

			upgradeSchedule := new(genesis.UpgradeScheduleDeployConfig)
			upgradeSchedule.ActivateForkAtGenesis(forks.Name(mode))
			upgradeOverridesJSON, err := json.Marshal(upgradeSchedule)
			if err != nil {
				panic(fmt.Errorf("failed to marshal upgrade schedule: %w", err))
			}
			var upgradeOverrides map[string]any
			if err := json.Unmarshal(upgradeOverridesJSON, &upgradeOverrides); err != nil {
				panic(fmt.Errorf("failed to unmarshal upgrade schedule: %w", err))
			}
			maps.Copy(baseUpgradeSchedule, upgradeOverrides)
			maps.Copy(intent.GlobalDeployOverrides, baseUpgradeSchedule)

			st := &state.State{
				Version: 1,
			}

			if err := deployer.ApplyPipeline(
				context.Background(),
				deployer.ApplyPipelineOpts{
					DeploymentTarget:   deployer.DeploymentTargetGenesis,
					L1RPCUrl:           "",
					DeployerPrivateKey: pk,
					Intent:             intent,
					State:              st,
					Logger:             lgr,
					StateWriter:        pipeline.NoopStateWriter(),
				},
			); err != nil {
				panic(fmt.Errorf("failed to apply pipeline: %w", err))
			}

			mtx.Lock()
			l2Alloc[mode] = st.Chains[0].Allocs.Data
			mtx.Unlock()

			// This needs to be updated whenever the latest hardfork is changed.
			if mode == genesis.L2AllocsGranite {
				dc, err := inspect.DeployConfig(st, intent.Chains[0].ID)
				if err != nil {
					panic(fmt.Errorf("failed to inspect deploy config: %w", err))
				}

				l1Contracts, err := inspect.L1(st, intent.Chains[0].ID)
				if err != nil {
					panic(fmt.Errorf("failed to inspect L1: %w", err))
				}

				// Set the L1 genesis block timestamp to now
				dc.L1GenesisBlockTimestamp = hexutil.Uint64(time.Now().Unix())
				dc.FundDevAccounts = true
				// Speed up the in memory tests
				dc.L1BlockTime = 2
				dc.L2BlockTime = 1
				dc.SetContracts(l1Contracts)
				mtx.Lock()
				deployConfigsByType[allocType] = dc
				l1AllocsByType[allocType] = st.L1StateDump.Data

				l1Deployments := genesis.CreateL1DeploymentsFromContracts(l1Contracts)
				l1DeploymentsByType[allocType] = l1Deployments
				mtx.Unlock()
			}
		}(mode)
	}
	wg.Wait()
	l2AllocsByType[allocType] = l2Alloc
}

func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, allocType AllocType) *state.Intent {
	secrets := secrets.DefaultSecrets
	addrs := secrets.Addresses()
	defaultPrestate := common.HexToHash("0x03c7ae758795765c6664a5d39bf63841c71ff191e9189522bad8ebff5d4eca98")
	genesisOutputRoot := common.HexToHash("0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF")
	return &state.Intent{
		ConfigType: state.IntentTypeCustom,
		L1ChainID:  900,
		SuperchainRoles: &addresses.SuperchainRoles{
			SuperchainProxyAdminOwner: deployer,
			ProtocolVersionsOwner:     deployer,
			SuperchainGuardian:        deployer,
			Challenger:                common.HexToAddress("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65"),
		},
		FundDevAccounts:    true,
		L1ContractsLocator: loc,
		L2ContractsLocator: loc,
		GlobalDeployOverrides: map[string]any{
			"maxSequencerDrift":                        300,
			"sequencerWindowSize":                      200,
			"channelTimeout":                           120,
			"l2OutputOracleSubmissionInterval":         10,
			"l2OutputOracleStartingTimestamp":          0,
			"l2OutputOracleProposer":                   addrs.Proposer,
			"l2OutputOracleChallenger":                 "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
			"l2GenesisBlockGasLimit":                   "0x1c9c380",
			"l1BlockTime":                              6,
			"baseFeeVaultMinimumWithdrawalAmount":      "0x8ac7230489e80000",
			"l1FeeVaultMinimumWithdrawalAmount":        "0x8ac7230489e80000",
			"sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000",
			"operatorFeeVaultMinimumWithdrawalAmount":  "0x8ac7230489e80000",
			"baseFeeVaultWithdrawalNetwork":            0,
			"l1FeeVaultWithdrawalNetwork":              0,
			"sequencerFeeVaultWithdrawalNetwork":       0,
			"operatorFeeVaultWithdrawalNetwork":        0,
			"finalizationPeriodSeconds":                2,
			"l2GenesisBlockBaseFeePerGas":              "0x1",
			"gasPriceOracleOverhead":                   2100,
			"gasPriceOracleScalar":                     1000000,
			"gasPriceOracleBaseFeeScalar":              1368,
			"gasPriceOracleBlobBaseFeeScalar":          801949,
			"gasPriceOracleOperatorFeeScalar":          0,
			"gasPriceOracleOperatorFeeConstant":        0,
			"l1CancunTimeOffset":                       "0x0",
			"faultGameAbsolutePrestate":                defaultPrestate.Hex(),
			"faultGameMaxDepth":                        50,
			"faultGameClockExtension":                  1,
			"faultGameMaxClockDuration":                1200,
			"faultGameGenesisBlock":                    0,
			"faultGameGenesisOutputRoot":               genesisOutputRoot.Hex(),
			"faultGameSplitDepth":                      14,
			"dangerouslyAllowCustomDisputeParameters":  true,
			"faultGameWithdrawalDelay":                 604800,
			"preimageOracleMinProposalSize":            10000,
			"preimageOracleChallengePeriod":            120,
			"proofMaturityDelaySeconds":                12,
			"disputeGameFinalityDelaySeconds":          6,
		},
		Chains: []*state.ChainIntent{
			{
				ID:                         common.BigToHash(big.NewInt(901)),
				BaseFeeVaultRecipient:      common.HexToAddress("0x14dC79964da2C08b23698B3D3cc7Ca32193d9955"),
				L1FeeVaultRecipient:        common.HexToAddress("0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f"),
				SequencerFeeVaultRecipient: common.HexToAddress("0xa0Ee7A142d267C1f36714E4a8F75612F20a79720"),
				OperatorFeeVaultRecipient:  common.HexToAddress("0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec"),
				Eip1559Denominator:         250,
				Eip1559DenominatorCanyon:   250,
				Eip1559Elasticity:          6,
				GasLimit:                   standard.GasLimit,
				Roles: state.ChainRoles{
					// Use deployer as L1PAO to deploy additional dispute impls
					L1ProxyAdminOwner: deployer,
					L2ProxyAdminOwner: deployer,
					SystemConfigOwner: deployer,
					UnsafeBlockSigner: common.HexToAddress("0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"),
					Batcher:           addrs.Batcher,
					Proposer:          addrs.Proposer,
					Challenger:        common.HexToAddress("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65"),
				},
				UseRevenueShare:    true,
				ChainFeesRecipient: common.HexToAddress("0xBcd4042DE499D14e55001CcbB24a551F3b954096"),
				AdditionalDisputeGames: []state.AdditionalDisputeGame{
					{
						ChainProofParams: state.ChainProofParams{
							// Alphabet game
							DisputeGameType:         255,
							DisputeAbsolutePrestate: defaultPrestate,
							DisputeMaxGameDepth:     14 + 3 + 1,
							DisputeSplitDepth:       14,
							DisputeClockExtension:   0,
							DisputeMaxClockDuration: 1200,
						},
						VMType: state.VMTypeAlphabet,
					},
					{
						ChainProofParams: state.ChainProofParams{
							DisputeGameType:         0,
							DisputeAbsolutePrestate: cannonPrestate(root, allocType),
							DisputeMaxGameDepth:     50,
							DisputeSplitDepth:       14,
							DisputeClockExtension:   0,
							DisputeMaxClockDuration: 1200,
						},
						VMType: cannonVMType(allocType),
					},
				},
			},
		},
	}
}

func ensureDir(dirPath string) error {
	stat, err := os.Stat(dirPath)
	if err != nil {
		return fmt.Errorf("failed to stat path: %w", err)
	}
	if !stat.IsDir() {
		return fmt.Errorf("path is not a directory")
	}
	return nil
}

func cannonVMType(allocType AllocType) state.VMType {
	if allocType == AllocTypeMTCannonNext {
		return state.VMTypeCannonNext
	}
	return state.VMTypeCannon
}

func IsCannonInDevelopment() bool {
	return cannonVMType(AllocTypeMTCannonNext).MipsVersion() != cannonVMType(AllocTypeMTCannon).MipsVersion()
}

type prestateFile struct {
	Pre string `json:"pre"`
}

var cannonPrestateMT common.Hash
var cannonPrestateMTNext common.Hash
var cannonPrestateMTOnce sync.Once
var cannonPrestateMTNextOnce sync.Once

func cannonPrestate(monorepoRoot string, allocType AllocType) common.Hash {
	var filename string

	var once *sync.Once
	var cacheVar *common.Hash
	cannonVmType := cannonVMType(allocType)
	if cannonVmType == state.VMTypeCannon {
		filename = "prestate-proof-mt64.json"
		once = &cannonPrestateMTOnce
		cacheVar = &cannonPrestateMT
	} else if cannonVmType == state.VMTypeCannonNext {
		if versions.GetCurrentVersion() != versions.GetExperimentalVersion() {
			filename = "prestate-proof-mt64Next.json"
		} else {
			filename = "prestate-proof-mt64.json"
		}
		once = &cannonPrestateMTNextOnce
		cacheVar = &cannonPrestateMTNext
	} else {
		panic("Unsupported cannon VM type: " + cannonVmType)
	}

	once.Do(func() {
		f, err := os.Open(path.Join(monorepoRoot, "op-program", "bin", filename))
		if err != nil {
			log.Warn("error opening prestate file. If you're running a test that requires prestates, make sure you've run `make cannon-prestates`", "err", err)
			return
		}
		defer f.Close()

		var prestate prestateFile
		dec := json.NewDecoder(f)
		if err := dec.Decode(&prestate); err != nil {
			log.Error("error decoding prestate file. If you're running a test that requires prestates, make sure you've run `make cannon-prestates`", "err", err)
			return
		}

		*cacheVar = common.HexToHash(prestate.Pre)
	})

	// Provide a dummy value so that the DeployDisputeGame script succeeds. Many tests do not require a dispute game. So this allieviates the need to build prestates during local development.
	if *cacheVar == (common.Hash{}) {
		*cacheVar = common.HexToHash("0xc02b59f772cb23a75b6ffb9f7602ba25fdd5d8e75ad88efcc013fec2c63b0895") // keccak("dummy")
	}
	return *cacheVar
}
